package com.ruoyi.exam.manager;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.thread.ThreadUtil;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.exam.domain.ExamRankingExt;
import com.ruoyi.exam.domain.ExamScores;
import com.ruoyi.exam.enums.ExamRankingSortValEnum;
import com.ruoyi.exam.mapper.ExamRankingExtMapper;
import com.ruoyi.exam.mapper.ExamScoresMapper;
import com.ruoyi.exam.vo.ExamRankingReqVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

import java.rmi.server.UID;
import java.util.*;
import java.util.stream.Collectors;

@Component
public class ExamScoreManager {
    @Autowired
    private RedisCache redisCache;
    @Autowired
    private ExamScoresMapper scoresMapper;
    @Autowired
    private ExamRankingExtMapper rankingExtMapper;

    private final String SCORE_RANKING_LOCK = "exam:score:ranking:lock";

    private final String SCORE_RANKING = "exam:score:ranking";
    private final String HANDS_ON_SCORE_RANKING = "exam:hands_on_score:ranking";
    private final String THEORY_SCORE_RANKING = "exam:theory_score:ranking";
    private final String MULTIPLE_TIME_RANKING = "exam:multiple_time:ranking";
    private final String HANDS_ON_TIME_RANKING = "exam:hands_on_time:ranking";
    private final String THEORY_TIME_RANKING = "exam:theory_time:ranking";


    /**
     * 添加排名
     * @param examId
     * @param uid
     * @param score
     * @param ranking
     * @return 移除新增项操作
     */
    public Runnable addRanking(int examId, long uid ,int score ,ExamRankingKey ranking){
        String key = buildKey(ranking ,examId);
        boolean exist = checkAndSetRanking(key ,true ,examId);
        redisCache.zsetAdd(key ,uid ,score);
        if(!exist){
            int prevExamId = scoresMapper.getPrevExamId(examId);
            clearRanking(prevExamId);
            if(rankingExtMapper.countExistForLock(examId) == 0){
                ExamRankingExt ext = new ExamRankingExt();
                ext.setExamId(examId);
                ext.setExistData(true);
                ext.setPersistence(false);
                rankingExtMapper.addExamRankingExt(ext);
            }else {
                rankingExtMapper.setExistData(examId);
            }
        }
        return () -> {
            redisCache.zsetRemove(key ,uid);
        };
    }

    /**
     * 获取排名
     * @param examId
     * @param uid
     * @param ranking
     * @return
     */
    public int getRank(int examId, long uid ,ExamRankingKey ranking){
        String key = buildKey(ranking ,examId);
        if(checkAndSetRanking(key ,false ,examId)){
            if(isRankingOfTime(key)){
                return (int)redisCache.zsetRank(key ,uid);
            }else {
                return (int)redisCache.zsetReverseRank(key ,uid);
            }
        }
        return -1;
    }

    /**
     * 获取排名区间
     * @param examId
     * @param start
     * @param end
     * @param ranking
     * @return
     */
    public List<ExamRanking> getRankRange(int examId, int start ,int end ,ExamRankingKey ranking){
        String key = buildKey(ranking ,examId);
        if(checkAndSetRanking(key ,false ,examId)){
            Set<ZSetOperations.TypedTuple> tuples;
            if(isRankingOfTime(key)){
                tuples = redisCache.zsetRangeWithScores(key ,start ,end);
            }else {
                tuples = redisCache.zsetReverseRangeWithScores(key ,start ,end);
            }
            List<ExamRanking> list = tuples.stream().map(tuple -> {
                long uid = (Integer) tuple.getValue();
                int rank = getRank(examId , uid,ranking) + 1;
                return new ExamRanking(uid ,rank ,tuple.getScore().intValue());
            }).collect(Collectors.toList());
            CollUtil.sort(list , Comparator.comparingInt(ExamRanking::getRank));
            return list;
        }
        return null;
    }

    private boolean isRankingOfTime(String key){
        return key.indexOf("time") > -1;
    }

    /**
     * 清理考试排行榜
     * @param examId
     */
    public void clearRanking(int examId){
        for(ExamRankingKey ranking : ExamRankingKey.values()){
            String key = buildKey(ranking ,examId);
            redisCache.deleteObject(key);
        }
    }

    private String buildKey(ExamRankingKey ranking ,int examId){
        String key = null;
        switch (ranking){
            case SCORES:
                key =  SCORE_RANKING;
                break;
            case HAND_ON_SCORE:
                key = HANDS_ON_SCORE_RANKING;
                break;
            case THEORY_SCORE:
                key = THEORY_SCORE_RANKING;
                break;
            case MULTIPLE_TIME:
                key = MULTIPLE_TIME_RANKING;
                break;
            case HAND_ON_TIME:
                key = HANDS_ON_TIME_RANKING;
                break;
            case THEORY_TIME:
                key = THEORY_TIME_RANKING;
                break;
        }
        return key + ":" + examId;
    }

    private void initScoreRanking(int examId){
        int count = 0;
        do{
            if(redisCache.tryLock(SCORE_RANKING_LOCK ,5000)){
                try {
                    if (!redisCache.hasKey(SCORE_RANKING)) {
                        doInitScoreRanking(examId);
                    }
                    break;
                }finally {
                    redisCache.unlock(SCORE_RANKING_LOCK);
                }
            }else {
                count ++;
                if(count > 5){
                    break;
                }
                ThreadUtil.sleep(10);
            }
        }while (true);
    }

    /**
     * 初始化缓存
     */
    private void doInitScoreRanking(int examId){
        List<ExamScores> scoresList = new ArrayList<>();
        ExamRankingReqVO reqVO = new ExamRankingReqVO();
        reqVO.setPageSize(200);
        reqVO.setExamId(examId);
        List<ExamScores> tmp;
        boolean scoresExist = redisCache.hasKey(SCORE_RANKING);
        boolean handsOnScoresExist = redisCache.hasKey(HANDS_ON_SCORE_RANKING);
        boolean theoryScoreExist = redisCache.hasKey(THEORY_SCORE_RANKING);
        boolean multipleTimeExist = redisCache.hasKey(MULTIPLE_TIME_RANKING);
        boolean handsOnTimeExist = redisCache.hasKey(HANDS_ON_TIME_RANKING);
        boolean theoryTimeExist = redisCache.hasKey(THEORY_TIME_RANKING);
        do{
            tmp = scoresMapper.pageScore(reqVO);
            scoresList.addAll(tmp);
            reqVO.setPageNo(reqVO.getPageNo() + 1);

            Set<ZSetOperations.TypedTuple> scoresTuples = new HashSet<>();
            Set<ZSetOperations.TypedTuple> handsOnScoreTuples = new HashSet<>();
            Set<ZSetOperations.TypedTuple> theoryScoreTuples = new HashSet<>();
            Set<ZSetOperations.TypedTuple> multipleTimeTuples = new HashSet<>();
            Set<ZSetOperations.TypedTuple> handsOnTimeTuples = new HashSet<>();
            Set<ZSetOperations.TypedTuple> theoryTimeTuples = new HashSet<>();
            for(ExamScores scores : tmp){
                if(scoresExist && scores.getScores() != null){
                    scoresTuples.add(ZSetOperations.TypedTuple.of(scores.getUid() , scores.getScores().doubleValue()));
                }
                if(handsOnScoresExist && scores.getHandsOnScore() != null){
                    handsOnScoreTuples.add(ZSetOperations.TypedTuple.of(scores.getUid() , scores.getHandsOnScore().doubleValue()));
                }
                if(theoryScoreExist && scores.getTheoryScore() != null){
                    theoryScoreTuples.add(ZSetOperations.TypedTuple.of(scores.getUid() , scores.getTheoryScore().doubleValue()));
                }
                if(multipleTimeExist && scores.getMultipleTime() != null){
                    multipleTimeTuples.add(ZSetOperations.TypedTuple.of(scores.getUid() , scores.getMultipleTime().doubleValue()));
                }
                if(handsOnTimeExist && scores.getHandsOnTime() != null){
                    handsOnTimeTuples.add(ZSetOperations.TypedTuple.of(scores.getUid() , scores.getHandsOnTime().doubleValue()));
                }
                if(theoryTimeExist && scores.getTheoryTime() != null){
                    theoryTimeTuples.add(ZSetOperations.TypedTuple.of(scores.getUid() , scores.getTheoryTime().doubleValue()));
                }
            }
            redisCache.zsetAdd(SCORE_RANKING ,scoresTuples);
            redisCache.zsetAdd(HANDS_ON_SCORE_RANKING ,handsOnScoreTuples);
            redisCache.zsetAdd(THEORY_SCORE_RANKING ,theoryScoreTuples);
            redisCache.zsetAdd(MULTIPLE_TIME_RANKING ,multipleTimeTuples);
            redisCache.zsetAdd(HANDS_ON_TIME_RANKING ,handsOnTimeTuples);
            redisCache.zsetAdd(THEORY_TIME_RANKING ,theoryTimeTuples);
        }while (tmp.size() >= reqVO.getPageSize());

    }

    /**
     * 排行榜不存在时初始化
     * @param key
     */
    private boolean checkAndSetRanking(String key ,boolean needToInitAllRanking ,int examId){
        if(!redisCache.hasKey(key)){
            if(needToInitAllRanking){
                initScoreRanking(examId);
            }else {
                return false;
            }
            return redisCache.hasKey(key);
        }else {
            return true;
        }
    }


    public class ExamRanking{
        private long uid;
        private int rank;
        private int score;

        public ExamRanking(long uid, int rank, int score) {
            this.uid = uid;
            this.rank = rank;
            this.score = score;
        }

        public long getUid() {
            return uid;
        }

        public int getRank() {
            return rank;
        }

        public int getScore() {
            return score;
        }
    }


    public enum ExamRankingKey {
        SCORES,
        HAND_ON_SCORE,
        THEORY_SCORE,

        MULTIPLE_TIME,
        HAND_ON_TIME,
        THEORY_TIME,
        ;

        public static ExamRankingKey parse(String examRankingKey){
            for(ExamRankingKey rankingKey : ExamRankingKey.values()){
                String key = rankingKey.name();
                if(key.equals(examRankingKey)){
                    return rankingKey;
                }
            }
            throw new IllegalArgumentException("exam ranking key undefined["+ examRankingKey +"]");
        }
    }

}
